// * 类继承，类继承是一个类扩展另一个类的一种方式。通过 extends 关键字。

// * 例子：
class Animal {
	constructor(name) {
		this.speed = 0
		this.name = name
	}

	run(speed) {
		this.speed = speed
		console.log(`${this.name} runs with speed ${this.speed}.`)
	}

	stop() {
		this.speed = 0
		console.log(`${this.name} stands still.`)
	}
}

// * 继承，extends 关键字将 Rabbit.prototype.[[prototype]] 设置为 Animal.prototype。
class Rabbit extends Animal {
	hide() {
		console.log(`${this.name} hides!`)
	}
}

let rabbit = new Rabbit('White Rabbit')

console.log(rabbit.name) // White Rabbit
rabbit.hide() // White Rabbit hides!

// *******************************************************************************************************************

// * 在 extends 后允许任意表达式

function f(phrse) {
	return class {
		sayHi() {
			console.log(phrse)
		}
	}
}

class U extends f('hello~') {}
const u = new U()

u.sayHi() // hello~
console.log(u)

// *******************************************************************************************************************

// * 重写方法

class Animal2 {
	speed = 0

	constructor(name) {
		this.name = name
	}

	run(speed) {
		this.speed = speed
		console.log(`${this.name} runs with speed ${this.speed}`)
	}

	stop() {
		this.speed = 0
		console.log(`${this.name} stands still.`)
	}
}

// * 执行 super.method(...) 来调用一个父类方法。
// * 执行 super(...) 来调用一个父类 constructor（只能在我们的 constructor 中）。
class Rabbit2 extends Animal2 {
	hide() {
		console.log(`${this.name} hides!`)
	}

	stop() {
		super.stop() // 调用父类的 stop
		this.hide()
	}
}

let r2 = new Rabbit2('Red Rabbit')

console.log(r2) // Rabbit2 { speed: 0, name: 'Red Rabbit' }

r2.run(5) // Red Rabbit runs with speed 5
rabbit.stop() // Red Rabbit stands still.

console.log(r2) // Rabbit2 { speed: 5, name: 'Red Rabbit' }

// *******************************************************************************************************************

// * 重写 constructor

// * 根据 规范，如果一个类扩展了另一个类并且没有 constructor，那么将生成下面这样的“空” constructor。

class B {
	constructor() {}
}

class A extends B {
	// * 如果没有定义 constructor 方法，则默认会生成一个空的 constructor 方法。
	constructor(...args) {
		// * 继承类的 constructor 必须调用 super(...)，并且一定要在使用 this 之前调用。
		super(...args) // * 调用了父类的 constructor，并传递了所有的参数。
	}
}

// * 继承一个类且自身实现了 constructor 方法，则必须调用 spuer() 的原因如下：
// * 当通过 new 执行一个常规函数时，它将创建一个空对象，并将这个空对象赋值给 this。
// * 但是当继承的 constructor 执行时，它不会执行此操作。它期望父类的 constructor 来完成这项工作（this 对象的创建）。
// * 因此，派生的 constructor 必须调用 super 才能执行其父类（base）的 constructor，否则 this 指向的那个对象将不会被创建。并且我们会收到一个报错。

// * 例子：

class Animal3 {
	constructor(name) {
		this.speed = 0
		this.name = name
	}

	fn() {
		console.log(this)
	} // * 存在于 Animal3.prototype 中
}

class Rabbit3 extends Animal3 {
	constructor(name, earLength) {
		super(name) // 必须调用父类的 constructor，且在 this 之前调用。
		this.earLength = earLength
	}
}

let r3 = new Rabbit3('White Rabbit', 4)

// * fn 存在于，r3.[[prototype]](Rabbit3.prototype).[[prototype]](Animal3.prototype) 中。
r3.fn() // Rabbit3 { speed: 0, name: 'White Rabbit', earLength: 4 }
console.log(r3) // Rabbit3 { speed: 0, name: 'White Rabbit', earLength: 4 }

// *******************************************************************************************************************

// * 重写类字段：一个棘手的注意要点。

// * 注意要点：关于类字段，父类构造器总是会使用它自己字段的值，而不是被重写的那一个。

// * 造成这个问题的原因，在于类字段初始化的顺序，类字段是这样初始化的：
// * 1. 对于基类（还未继承任何东西的那种），在构造函数调用前初始化。
// * 2. 对于派生类，在 super() 后立刻初始化。
// * 这种字段与方法之间微妙的区别只特定于 JavaScript。我们可以通过使用方法或者 getter/setter 替代类字段，来修复这个问题。

class Animal4 {
	name = 'animal4'

	constructor() {
		console.log(this.name)
		this.showName()
	}

	showName() {
		console.log('animal4')
	}
}

class Rabbit4 extends Animal4 {
	// 在这里没有指定 constructor 方法，它会隐式创建一个空的 constructor 方法，并通过 super(...) 调用父类的 constructor。
	// constructor(...args) {
	// 	super(...args)
	// }

	name = 'rabbit4'

	showName() {
		console.log('rabbit4')
	}
}

new Animal4() // animal4 animal4
new Rabbit4() // animal4 rabbit4

// *******************************************************************************************************************

// * [[HomeObject]]：当一个函数被定义为 类 或者 对象方法 时，它（方法）的 [[HomeObject]] 属性就成为了该对象。然后 super 使用它来解析父原型及其方法。

// * 函数通常都是“自由”的，并没有绑定到 JavaScript 中的对象，它们可以在对象之间复制，并用另外一个 this 调用它。 但 [[HomeObject]] 不能被更改，所以这个绑定是永久的。

// * 例子：

let obj = {
	name: 'Obj',

	// obj.eat.[[HomeObject]] = obj
	eat() {
		console.log(this.name)
	},
}

let obj2 = {
	__proto__: obj,
	name: 'Obj2',

	// obj2.eat.[[HomeObject]] = obj2
	eat() {
		super.eat()
	},
}

let obj3 = {
	__proto__: obj2,
	name: 'Obj3',

	// obj3.eat.[[HomeObject]] = obj3
	eat() {
		super.eat()
	},
}

obj3.eat() // Obj3 （基于 [[HomeObject]] 运行机制按照预期执行，并没有使用 this）

// * super 关键字用于访问父对象的方法。

// * [[HomeObject]] 的作用正是确保在使用 super 关键字时，当前对象能够正确地找到其父对象。这一属性指向定义方法的对象，使得super能够在调用时解析出正确的父原型。

// *******************************************************************************************************************

// * 方法，不是函数属性

// * 例子：

let o = {
	func() {
		console.log('o')
	},
}

let objs = {
	__proto__: o,

	// * 这样定义的函数叫“方法”。它的 [[HomeObject]] 属性指向 objs
	fn() {
		super.func()
	},

	// * 这样定义的函数叫“属性方法”。它设置 [[HomeObject]] 属性。
	fn2: function () {
		// * 报错，因为 fn2 没有指定 [[HomeObject]] 所以这里不能定义 super 这段代码。
		// super.func()
	},
}

objs.fn() // o
